#include "BufferView.h"

#include <QActionGroup>
#include <QMouseEvent>
#include <QPainter>
#include <QScreen>
#include <QScrollBar>
#include <QStyle>
#include <QStyleOption>
#include <QTextLayout>
#include <QWindow>
#include "ctype.h"

Q_DECLARE_METATYPE(bytes_view_type)

BufferView::BufferView(FERange range, QWidget *parent) :
    QAbstractScrollArea(parent),
    layout_(new QTextLayout()),
    range_(range),
    show_offset_(true),
    show_hex_(true),
    font_width_(0),
    line_height_(0)
{
    gui_bytes_view_ = BYTES_HEX;
    row_width_ = gui_bytes_view_ == BYTES_HEX ? 16 : 8;

    layout_->setCacheEnabled(true);

    offset_normal_fg_ = alphaBlend(palette().windowText().color(), palette().window().color(), 0.35);
    offset_field_fg_ = alphaBlend(palette().windowText().color(), palette().window().color(), 0.65);

    window()->winId(); // Required for screenChanged? https://phabricator.kde.org/D20171
    connect(window()->windowHandle(), &QWindow::screenChanged, viewport(), [=](const QScreen *) { viewport()->update(); });

    createContextMenu();

//    setMouseTracking(true);

#ifdef Q_OS_MAC
    setAttribute(Qt::WA_MacShowFocusRect, true);
#endif
}

BufferView::~BufferView()
{
    ctx_menu_.clear();
    delete(layout_);
}

void BufferView::createContextMenu()
{
    QActionGroup * format_actions = new QActionGroup(this);
    action_bytes_hex_ = format_actions->addAction(tr("Show bytes as hexadecimal"));
    action_bytes_hex_->setData(QVariant::fromValue(BYTES_HEX));
    action_bytes_hex_->setCheckable(true);

    action_bytes_bits_ = format_actions->addAction(tr("…as bits"));
    action_bytes_bits_->setData(QVariant::fromValue(BYTES_BITS));
    action_bytes_bits_->setCheckable(true);

    ctx_menu_.addActions(format_actions->actions());
    connect(format_actions, &QActionGroup::triggered, this, &BufferView::setHexDisplayFormat);

//    ctx_menu_.addSeparator();

    updateContextMenu();
}

void BufferView::updateContextMenu()
{
    switch (gui_bytes_view_) {
    case BYTES_HEX:
        action_bytes_hex_->setChecked(true);
        break;
    case BYTES_BITS:
        action_bytes_bits_->setChecked(true);
        break;
    }
}

bool BufferView::isEmpty() const
{
    return ((FEBuffer *)(range_.buffer))->data_len == 0;
}

QSize BufferView::minimumSizeHint() const
{
    // Allow panel to shrink to any size
    return QSize();
}

void BufferView::setMonospaceFont(const QFont &mono_font)
{
    QFont int_font(mono_font);

    setFont(int_font);
    viewport()->setFont(int_font);
    layout_->setFont(int_font);

    updateLayoutMetrics();

    updateScrollbars();
    viewport()->update();
}

void BufferView::updateByteViewSettings()
{
    row_width_ = gui_bytes_view_ == BYTES_HEX ? 16 : 8;

    updateContextMenu();
    updateScrollbars();
    viewport()->update();
}

void BufferView::markRange(FERange range)
{
    if(0 == memcmp(&range_, &range, sizeof(FERange)))
        return;

    range_ = range;
    scrollToByte(range_.bit_offset >> 3);
    viewport()->update();
}

void BufferView::paintEvent(QPaintEvent *)
{
//    updateLayoutMetrics();

    QPainter painter(viewport());
    painter.translate(-horizontalScrollBar()->value() * font_width_, 0);

    // Pixel offset of this row
    int row_y = 0;

    // Starting byte offset
    int offset = verticalScrollBar()->value() * row_width_;

    // Clear the area
    painter.fillRect(viewport()->rect(), palette().base());

    // Offset background. We want the entire height to be filled.
    if (show_offset_) {
        QRect offset_rect = QRect(viewport()->rect());
        offset_rect.setWidth(offsetPixels());
        painter.fillRect(offset_rect, palette().window());
    }

    if (((FEBuffer *)(range_.buffer))->data_len == 0) {
        return;
    }

    // Data rows
    int widget_height = height();
//    painter.save();

    while ((int) (row_y + line_height_) < widget_height && offset < ((FEBuffer *)(range_.buffer))->data_len) {
        drawLine(&painter, offset, row_y);
        offset += row_width_;
        row_y += line_height_;
    }

//    painter.restore();

//    QStyleOptionFocusRect option;
//    option.initFrom(this);
//    style()->drawPrimitive(QStyle::PE_FrameFocusRect, &option, &painter, this);
}

//void BufferView::resizeEvent(QResizeEvent *)
//{
//    updateScrollbars();
//}

void BufferView::contextMenuEvent(QContextMenuEvent *event)
{
    ctx_menu_.popup(event->globalPos());
}

// Private

const int BufferView::separator_interval_ = 8;

void BufferView::updateLayoutMetrics()
{
    font_width_  = stringWidth("M");
    // We might want to match ProtoTree::rowHeight.
    line_height_ = fontMetrics().lineSpacing();
}

int BufferView::stringWidth(const QString &line)
{
#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
    return fontMetrics().horizontalAdvance(line);
#else
    return fontMetrics().boundingRect(line).width();
#endif
}

// Draw a line of byte view text for a given bit_offset.
// Text highlighting is handled using QTextLayout::FormatRange.
void BufferView::drawLine(QPainter *painter, const int offset, const int row_y)
{
//    if (isEmpty()) {
//        return;
//    }

    int tvb_len = ((FEBuffer *)(range_.buffer))->data_len;
    int max_tvb_pos = qMin(offset + row_width_, tvb_len) - 1;
    char *data = fetchFEBuffer(((FEBuffer *)(range_.buffer)), offset, max_tvb_pos - offset + 1);
    QList<QTextLayout::FormatRange> fmt_list;

    static const char hexchars[16] = {
        '0', '1', '2', '3', '4', '5', '6', '7',
        '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };

    QString line;
    // HighlightMode offset_mode = ModeOffsetField;

    // Offset.
    if (show_offset_) {
        line = QString(" %1 ").arg(offset, offsetChars(false), 16, QChar('0'));
    }

    // Hex
    if (show_hex_) {
        int ascii_start = static_cast<int>(line.length()) + hexChars() + 3;
        // Extra hover space before and after each byte.
        int slop = font_width_ / 2;

        for (int tvb_pos = offset; tvb_pos <= max_tvb_pos; tvb_pos++) {
            line += ' ';
            /* insert a space every separator_interval_ bytes */
            if ((tvb_pos != offset) && ((tvb_pos % separator_interval_) == 0)) {
                line += ' ';
            }

            switch (gui_bytes_view_) {
            case BYTES_HEX:
                line += hexchars[(data[tvb_pos - offset] & 0xf0) >> 4];
                line += hexchars[data[tvb_pos - offset] & 0x0f];
                break;
            case BYTES_BITS:
                /* XXX, bitmask */
                for (int j = 7; j >= 0; j--) {
                    line += (data[tvb_pos - offset] & (1 << j)) ? '1' : '0';
                }
                break;
            }
        }
        line += QString(ascii_start - line.length(), ' ');
        addHexFormatRange(fmt_list, range_.bit_offset, range_.bit_len, offset, max_tvb_pos, ModeField);
    }

    // ASCII
//    bool in_non_printable = false;
//    int np_start = 0;
//    int np_len = 0;
    char c;

    for (int tvb_pos = offset; tvb_pos <= max_tvb_pos; tvb_pos++) {
        /* insert a space every separator_interval_ bytes */
        if ((tvb_pos != offset) && ((tvb_pos % separator_interval_) == 0)) {
            line += ' ';
        }

        c = data[tvb_pos - offset];

        if (isprint((uchar)c)) {
            line += c;
//            if (in_non_printable) {
//                in_non_printable = false;
//                addAsciiFormatRange(fmt_list, np_start, np_len, offset, max_tvb_pos, ModeNonPrintable);
//            }
        } else {
            line += UTF8_MIDDLE_DOT;
//            if (!in_non_printable) {
//                in_non_printable = true;
//                np_start = tvb_pos;
//                np_len = 1;
//            } else {
//                np_len++;
//            }
        }
    }
//    if (in_non_printable) {
//        addAsciiFormatRange(fmt_list, np_start, np_len, offset, max_tvb_pos, ModeNonPrintable);
//    }
    
    addAsciiFormatRange(fmt_list, range_.bit_offset, range_.bit_len, offset, max_tvb_pos, ModeField);

    // XXX Fields won't be highlighted if neither hex nor ascii are enabled.
//    addFormatRange(fmt_list, 0, offsetChars(), offset_mode);

    layout_->clearLayout();
    layout_->clearFormats();
    layout_->setText(line);
    layout_->setFormats(fmt_list.toVector());
    layout_->beginLayout();
    QTextLine tl = layout_->createLine();
    tl.setLineWidth(totalPixels());
    tl.setLeadingIncluded(true);
    layout_->endLayout();
    layout_->draw(painter, QPointF(0.0, row_y));
}

bool BufferView::addFormatRange(QList<QTextLayout::FormatRange> &fmt_list, int start, int length, HighlightMode mode)
{
    if (length < 1)
        return false;

    QTextLayout::FormatRange format_range;
    format_range.start = start;
    format_range.length = length;
    switch (mode) {
    case ModeNormal:
        return false;
    case ModeField:
        format_range.format.setBackground(palette().highlight());
        format_range.format.setForeground(palette().highlightedText());
        break;
    case ModeProtocol:
        format_range.format.setBackground(palette().window());
        format_range.format.setForeground(palette().windowText());
        break;
    case ModeOffsetNormal:
        format_range.format.setForeground(offset_normal_fg_);
        break;
    case ModeOffsetField:
        format_range.format.setForeground(offset_field_fg_);
        break;
    case ModeNonPrintable:
        format_range.format.setForeground(offset_normal_fg_);
        break;
    }
    fmt_list << format_range;
    return true;
}

bool BufferView::addHexFormatRange(QList<QTextLayout::FormatRange> &fmt_list, uint64_t mark_bit_start, uint64_t mark_bit_length, int tvb_offset, int max_tvb_pos, BufferView::HighlightMode mode)
{
    uint64_t mark_bit_end = mark_bit_start + mark_bit_length - 1;
    if ((mark_bit_start >> 3) > max_tvb_pos ||  (mark_bit_end >> 3) < tvb_offset) return false;

    int fmt_start = 0;
    int fmt_length = 0;
    if (gui_bytes_view_ == BYTES_HEX) {
        int mark_start = mark_bit_start >> 3;
        int mark_end = mark_bit_end >> 3;
        int chars_per_byte = 2;
        int chars_plus_pad = chars_per_byte + 1;
        int byte_start = qMax(tvb_offset, mark_start) - tvb_offset;
        int byte_end = qMin(max_tvb_pos, mark_end) - tvb_offset;
        fmt_start = offsetChars() + 1 // offset + spacing
                        + (byte_start / separator_interval_)
                        + (byte_start * chars_plus_pad);
        fmt_length = offsetChars() + 1 // offset + spacing
                         + (byte_end / separator_interval_)
                         + (byte_end * chars_plus_pad)
                         + chars_per_byte
                         - fmt_start;
    } else {
        uint64_t tvb_bit_offset = tvb_offset << 3;
        uint64_t max_tvb_bit_pos = ((max_tvb_pos + 1) << 3) - 1;
        uint64_t char_start = qMax(tvb_bit_offset, mark_bit_start) - tvb_bit_offset;
        uint64_t char_end = qMin(max_tvb_bit_pos, mark_bit_end) - tvb_bit_offset;
        fmt_start = offsetChars() + 1 // offset + spacing
                    + (char_start >> 3) / separator_interval_
                    + (char_start >>3)
                    + char_start;
        fmt_length = offsetChars() + 1 // offset + spacing
                     + (char_end >> 3) / separator_interval_
                     + (char_end >> 3)
                     + char_end
                     + 1
                     - fmt_start;
    }
    return addFormatRange(fmt_list, fmt_start, fmt_length, mode);
}

bool BufferView::addAsciiFormatRange(QList<QTextLayout::FormatRange> &fmt_list, uint64_t mark_bit_start, uint64_t mark_bit_length, int tvb_offset, int max_tvb_pos, BufferView::HighlightMode mode)
{
    int mark_start = mark_bit_start >> 3;
    int mark_end = (mark_bit_start + mark_bit_length - 1) >> 3;
    if (mark_start > max_tvb_pos && mark_end < tvb_offset) return false;

    int byte_start = qMax(tvb_offset, mark_start) - tvb_offset;
    int byte_end = qMin(max_tvb_pos, mark_end) - tvb_offset;
    int fmt_start = offsetChars() + hexChars() + 3 // offset + hex + spacing
            + (byte_start / separator_interval_)
            + byte_start;
    int fmt_length = offsetChars() + hexChars() + 3 // offset + hex + spacing
            + (byte_end / separator_interval_)
            + byte_end
            + 1 // Just one character.
            - fmt_start;
    return addFormatRange(fmt_list, fmt_start, fmt_length, mode);
}

void BufferView::scrollToByte(int byte)
{
    verticalScrollBar()->setValue(byte / row_width_);
}

// Offset character width
int BufferView::offsetChars(bool include_pad)
{
    int padding = include_pad ? 2 : 0;
    if (! isEmpty() && ((FEBuffer *)(range_.buffer))->data_len > 0xffff) {
        return 8 + padding;
    }
    return 4 + padding;
}

// Offset pixel width
int BufferView::offsetPixels()
{
    if (show_offset_) {
        // One pad space before and after
        QString zeroes = QString(offsetChars(), '0');
        return stringWidth(zeroes);
    }
    return 0;
}

// Hex pixel width
int BufferView::hexPixels()
{
    if (show_hex_) {
        // One pad space before and after
        QString zeroes = QString(hexChars() + 2, '0');
        return stringWidth(zeroes);
    }
    return 0;
}

int BufferView::asciiPixels()
{
    // Two pad spaces before, one after
    int ascii_chars = (row_width_ + ((row_width_ - 1) / separator_interval_));
    QString zeroes = QString(ascii_chars + 3, '0');
    return stringWidth(zeroes);
    return 0;
}

int BufferView::totalPixels()
{
    return offsetPixels() + hexPixels() + asciiPixels();
}

QRgb BufferView::alphaBlend(const QColor &color1, const QColor &color2, qreal alpha)
{
    alpha = qBound(qreal(0.0), alpha, qreal(1.0));

    int r1 = color1.red() * alpha;
    int g1 = color1.green() * alpha;
    int b1 = color1.blue() * alpha;
    int r2 = color2.red() * (1 - alpha);
    int g2 = color2.green() * (1 - alpha);
    int b2 = color2.blue() * (1 - alpha);

    QColor alpha_color(r1 + r2, g1 + g2, b1 + b2);
    return alpha_color.rgb();
}

int BufferView::hexChars()
{
    int row_width = gui_bytes_view_ == BYTES_HEX ? 16 : 8;
    int chars_per_byte = gui_bytes_view_ == BYTES_HEX ? 3 : 9;
    return (row_width * chars_per_byte) + ((row_width - 1) / separator_interval_);
}

// We do chunky (per-character) scrolling because it makes some of the
// math easier. Should we do smooth scrolling?
void BufferView::updateScrollbars()
{
    const int length = static_cast<int>(((FEBuffer *)(range_.buffer))->data_len);
    if (length > 0) {
        int all_lines_height = length / row_width_ + ((length % row_width_) ? 1 : 0) - viewport()->height() / line_height_;

        verticalScrollBar()->setRange(0, qMax(0, all_lines_height));
        horizontalScrollBar()->setRange(0, qMax(0, int((totalPixels() - viewport()->width()) / font_width_)));
    }
}

void BufferView::setHexDisplayFormat(QAction *action)
{
    if (!action) {
        return;
    }

    gui_bytes_view_ = action->data().value<bytes_view_type>();

    updateByteViewSettings();
//    emit byteViewSettingsChanged();
}
